import sys
sys.path.append('/opt/lib/python3.11/site-packages')
import json
import requests
import time
import os
import logging
import argparse
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from pypinyin import pinyin, Style
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta
import tempfile

class KodiDBHandler(FileSystemEventHandler):
    """处理Kodi数据库文件变化的事件处理器"""
    
    def __init__(self, kodi_updater):
        self.updater = kodi_updater
        self.last_updated = datetime.now() - timedelta(minutes=5)  # 初始化为5分钟前
        self.logger = logging.getLogger('KodiDBHandler')
        self.script_update_marker = self._create_marker_file()
        
    def _create_marker_file(self):
        """创建临时标记文件路径"""
        return os.path.join(tempfile.gettempdir(), "kodi_pinyin_updater_marker")
        
    def on_modified(self, event):
        """文件修改事件处理"""
        if not event.is_directory and self._is_video_db(event.src_path):
            current_time = datetime.now()
            
            # 防抖机制：确保事件间隔至少1分钟，避免重复触发
            if (current_time - self.last_updated).total_seconds() < 60:
                self.logger.info(f"忽略短时间内的重复更新请求: {event.src_path}")
                return
                
            self.logger.info(f"检测到数据库文件变更: {event.src_path}")
            
            # 检查是否是脚本自身更新导致的变更
            if self._is_script_update():
                self.logger.info("检测到这是脚本自身更新导致的变更，忽略")
                self._clear_marker()
                return
                
            # 等待一段时间确保写入完成
            time.sleep(5)
            
            # 检查Kodi是否正在扫描
            if self.updater.is_kodi_scanning():
                self.logger.info("Kodi正在扫描，等待扫描完成...")
                # 等待扫描完成
                while self.updater.is_kodi_scanning():
                    time.sleep(10)
                time.sleep(10)  # 额外等待10秒确保完全完成
                
            # 执行更新
            try:
                self._set_marker()  # 设置标记，表示这是脚本更新
                self.logger.info("开始更新拼音索引...")
                self.updater.run()
                self.last_updated = current_time
                self.logger.info("拼音索引更新完成")
            except Exception as e:
                self.logger.error(f"更新拼音索引失败: {str(e)}")
            finally:
                self._clear_marker()  # 确保清除标记
    
    def _is_video_db(self, file_path):
        """检查是否是视频相关的数据库文件"""
        filename = os.path.basename(file_path)
        return 'video' in filename.lower() and filename.endswith('.db')
        
    def _set_marker(self):
        """设置脚本更新标记"""
        try:
            with open(self.script_update_marker, 'w') as f:
                f.write(str(time.time()))
        except Exception as e:
            self.logger.error(f"设置更新标记失败: {str(e)}")
    
    def _clear_marker(self):
        """清除脚本更新标记"""
        try:
            if os.path.exists(self.script_update_marker):
                os.remove(self.script_update_marker)
        except Exception as e:
            self.logger.error(f"清除更新标记失败: {str(e)}")
    
    def _is_script_update(self):
        """检查是否是脚本自身更新"""
        return os.path.exists(self.script_update_marker)

class KodiPinyinUpdater:
    """Kodi拼音索引更新器"""
    
    def __init__(self, host='localhost', port=8080, username=None, password=None, 
                 guisettings_path=None, totals_file='~/.kodi_pinyin_totals.json',
                 log_file='~/.kodi_pinyin_updater.log', 
                 first_run_flag='~/.kodi_pinyin_first_run'):  # 添加首次运行标记文件参数
        # 配置日志
        self._setup_logging(os.path.expanduser(log_file))
        
        # 自动获取guisettings.xml路径
        if not guisettings_path:
            default_path = os.path.expanduser("~/.kodi/userdata/guisettings.xml")
            if os.path.exists(default_path):
                guisettings_path = default_path
            else:
                raise FileNotFoundError("未找到默认的guisettings.xml文件，请手动指定路径")

        # 解析XML获取用户名和密码
        self.username, self.password = self._parse_guisettings(guisettings_path)
        
        self.url = f"http://{host}:{port}/jsonrpc"
        self.auth = (self.username, self.password)
        self.marker = "#_#"
        self.totals_file = os.path.expanduser(totals_file)
        self.first_run_flag = os.path.expanduser(first_run_flag)  # 初始化首次运行标记文件路径
        self.logger = logging.getLogger('KodiPinyinUpdater')

    def _setup_logging(self, log_file):
        """配置日志系统"""
        logger = logging.getLogger()
        logger.setLevel(logging.INFO)
        
        # 创建文件处理器
        file_handler = logging.FileHandler(log_file)
        file_handler.setLevel(logging.INFO)
        
        # 创建控制台处理器
        console_handler = logging.StreamHandler()
        console_handler.setLevel(logging.INFO)
        
        # 创建格式化器并添加到处理器
        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
        file_handler.setFormatter(formatter)
        console_handler.setFormatter(formatter)
        
        # 添加处理器到logger
        logger.addHandler(file_handler)
        logger.addHandler(console_handler)

    def _parse_guisettings(self, path):
        """解析guisettings.xml获取Web服务器凭据"""
        try:
            tree = ET.parse(path)
            root = tree.getroot()
            
            # 定位用户名和密码节点
            username_node = root.find(".//setting[@id='services.webserverusername']")
            password_node = root.find(".//setting[@id='services.webserverpassword']")
            
            username = username_node.text if username_node is not None else "kodi"
            password = password_node.text if password_node is not None else ""
            
            return username, password
        except Exception as e:
            raise ValueError(f"解析guisettings.xml失败: {str(e)}")

    def get_pinyin_initials(self, text):
        """生成首拼首字母"""
        if not text:
            return ""
        initials = pinyin(text, style=Style.FIRST_LETTER)
        pinyin_part = ''.join([i[0].upper() for i in initials if i])
        return f"{pinyin_part}{self.marker}{text}"

    def call_api(self, method, params):
        """通用API调用方法"""
        try:
            response = requests.post(
                self.url,
                auth=self.auth,
                headers={'Content-Type': 'application/json'},
                data=json.dumps({
                    "jsonrpc": "2.0",
                    "id": 1,
                    "method": method,
                    "params": params
                })
            )
            response.raise_for_status()  # 检查HTTP状态码
            return response.json()
        except Exception as e:
            self.logger.error(f"API调用失败: {str(e)}")
            return None

    def get_totals(self):
        """获取当前媒体库总数"""
        try:
            # 获取电影总数
            movies = self.call_api("VideoLibrary.GetMovies", {"properties": []})
            if not movies or 'result' not in movies:
                raise ValueError("获取电影总数失败")
            movie_total = movies['result'].get('limits', {}).get('total', 0)

            # 获取电视剧总数
            tvshows = self.call_api("VideoLibrary.GetTVShows", {"properties": []})
            if not tvshows or 'result' not in tvshows:
                raise ValueError("获取电视剧总数失败")
            tvshow_total = tvshows['result'].get('limits', {}).get('total', 0)

            return movie_total, tvshow_total
        except Exception as e:
            raise RuntimeError(f"获取媒体总数失败: {str(e)}")

    def load_totals(self):
        """从本地文件加载上次记录的总数"""
        try:
            with open(self.totals_file, 'r') as f:
                data = json.load(f)
                return data.get('movie_total', 0), data.get('tvshow_total', 0)
        except (FileNotFoundError, json.JSONDecodeError):
            return 0, 0

    def save_totals(self, movie_total, tvshow_total):
        """保存当前总数到本地文件"""
        with open(self.totals_file, 'w') as f:
            json.dump({
                'movie_total': movie_total,
                'tvshow_total': tvshow_total
            }, f)

    def process_media(self, media_type):
        """通用处理逻辑（支持movie/tvshow）"""
        # 确定API方法
        get_method = f"VideoLibrary.Get{media_type.capitalize()}s"
        set_method = f"VideoLibrary.Set{media_type.capitalize()}Details"
        id_field = f"{media_type}id"  # movieid/tvshowid

        # 分页参数配置
        page_size = 200  # 每页最大处理量
        current_offset = 0
        total_updated = 0

        self.logger.info(f"开始处理{media_type}...")
        
        while True:
            # 动态计算请求范围
            request_params = {
                "properties": ["title", "originaltitle"],
                "filter": {
                    "field": "originaltitle",
                    "operator": "doesnotcontain",
                    "value": self.marker
                },
                "limits": {"start": current_offset, "end": current_offset + page_size},
                "sort": {"order": "ascending", "method": "title"}  # 增加稳定排序
            }

            # API请求
            result = self.call_api(get_method, request_params)

            # 错误处理
            if not result or 'result' not in result:
                self.logger.error(f"获取{media_type}数据失败，当前偏移量：{current_offset}")
                break

            items = result['result'].get(f"{media_type}s", [])
            if not items:
                break  # 无更多数据时退出循环

            # 处理当前页数据
            page_updated = 0
            for item in items:
                # 跳过已处理项（二次验证）
                if self.marker in item.get('originaltitle', ''):
                    continue

                # 生成拼音标记
                new_value = self.get_pinyin_initials(item.get('title', ''))

                # 更新记录
                update_result = self.call_api(set_method, {
                    id_field: item[id_field],
                    "originaltitle": new_value
                })

                # 结果验证
                if update_result and 'error' not in update_result:
                    page_updated += 1
                else:
                    self.logger.error(f"更新失败：{item.get('title')} | 响应：{json.dumps(update_result)}")
            
            # 动态调整偏移量
            actual_count = len(items)
            current_offset += actual_count  # 根据实际返回数量推进偏移量
            total_updated += page_updated
        
            self.logger.info(f"当前偏移量：{current_offset} | 本页更新：{page_updated} | 累计更新：{total_updated}")
            
            # 提前退出条件：当实际返回数量小于请求量时
            if actual_count < page_size:
                break
                
        self.logger.info(f"{media_type}处理完成，共更新{total_updated}条记录")
        return total_updated

    def is_kodi_scanning(self):
        """检查Kodi是否正在扫描媒体库"""
        result = self.call_api("VideoLibrary.GetScanState", {})
        if result and 'result' in result:
            return result['result'].get('scanning', False)
        return False

    def run(self):
        """主运行逻辑"""
        try:
            self.logger.info("开始执行拼音索引更新...")
            
            # 检查Kodi是否正在扫描
            if self.is_kodi_scanning():
                self.logger.info("检测到Kodi正在扫描，等待扫描完成...")
                while self.is_kodi_scanning():
                    time.sleep(10)
                self.logger.info("Kodi扫描完成")
                time.sleep(10)  # 额外等待10秒确保完全完成
            
            # 获取当前总数
            current_movie_total, current_tvshow_total = self.get_totals()
            self.logger.info(f"当前媒体库：电影-{current_movie_total}部，电视剧-{current_tvshow_total}部")
            
            # 读取上次记录
            last_movie_total, last_tvshow_total = self.load_totals()
            self.logger.info(f"上次更新记录：电影-{last_movie_total}部，电视剧-{last_tvshow_total}部")

            # 执行更新
            movie_count = self.process_media('movie')
            tvshow_count = self.process_media('tvshow')

            # 保存新总数
            self.save_totals(current_movie_total, current_tvshow_total)

            summary_message = f"更新完成！电影：{movie_count}条，电视剧：{tvshow_count}条"
            self.logger.info(summary_message)

        except Exception as e:
            self.logger.error(f"运行错误: {str(e)}")

    def start_watching(self, watch_dir):
        """启动文件监控"""
        # 检查是否是首次运行
        if not self._check_first_run():
            self.logger.info("首次启动，执行初始更新...")
            self.run()
            self._mark_as_run()  # 标记为已运行
        else:
            self.logger.info("非首次启动，跳过初始更新")
            
        event_handler = KodiDBHandler(self)
        observer = Observer()
        observer.schedule(event_handler, path=watch_dir, recursive=False)
        observer.start()
        
        self.logger.info(f"开始监控目录: {watch_dir}")
        self.logger.info("等待数据库文件变化...")
        
        try:
            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            observer.stop()
        observer.join()

    def _check_first_run(self):
        """检查是否是首次运行"""
        return os.path.exists(self.first_run_flag)

    def _mark_as_run(self):
        """标记为已运行"""
        try:
            with open(self.first_run_flag, 'w') as f:
                f.write(str(datetime.now()))
            self.logger.info(f"已创建首次运行标记文件: {self.first_run_flag}")
        except Exception as e:
            self.logger.error(f"创建首次运行标记文件失败: {str(e)}")

def main():
    parser = argparse.ArgumentParser(description='Kodi拼音索引自动更新工具')
    parser.add_argument('--host', default='localhost', help='Kodi JSON-RPC服务主机')
    parser.add_argument('--port', type=int, default=8080, help='Kodi JSON-RPC服务端口')
    parser.add_argument('--guisettings', help='guisettings.xml文件路径')
    parser.add_argument('--watch-dir', default='/storage/.kodi/userdata/Database', 
                        help='监控的数据库目录')
    parser.add_argument('--run-once', action='store_true', help='仅运行一次更新，不启动监控')
    parser.add_argument('--first-run-flag', default='~/.kodi_pinyin_first_run', 
                        help='首次运行标记文件路径')  # 添加命令行参数
    
    args = parser.parse_args()
    
    try:
        updater = KodiPinyinUpdater(
            host=args.host,
            port=args.port,
            guisettings_path=args.guisettings,
            first_run_flag=args.first_run_flag  # 传递首次运行标记文件参数
        )
        
        if args.run_once:
            updater.run()
        else:
            updater.start_watching(args.watch_dir)
            
    except Exception as e:
        logging.error(f"程序初始化失败: {str(e)}")
        sys.exit(1)

if __name__ == '__main__':
    main()    